﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;

namespace RentIt06
{
    public class UserSession : IUserSession
    {
        Account account;

        public Account Login(string email, string password) {
            var dbResult = from u in new db.DataMapDataContext().Users
                           where u.email == email
                              && u.password == Account.hash(password, email)
                           select u;
            if (dbResult.Any()) account = new Account(dbResult.First());
            else throw new FaultException(new FaultReason("Wrong password or email"), new FaultCode("401"));
            return account;
        }

        private void checkIfLoggedIn()
        {
            if (account.Equals(default(Account))) throw new FaultException("You are not logged in");
        }

        public void Logout() {
            // DONE!
        }

        private db.User ThisUser(string password = "", db.DataMapDataContext map = null) {
            var result = (from u in (map ?? new db.DataMapDataContext()).Users
                          where u.id == account.Id
                                && (password == "" || u.password == Account.hash(password, account.Email))
                          select u);
            if (result.Any()) return result.First();
            throw new FaultException(new FaultReason("Wrong Password"), new FaultCode("403"));
        }

        public Account ReadMyAccount() {
            checkIfLoggedIn();
            return new Account(ThisUser());
        }

        public void UpdateAccount(Account info, string oldPassword) {
            checkIfLoggedIn();
            if (info.Id != account.Id) throw new FaultException("You must use the account object that is logged in with");
            var map = new db.DataMapDataContext();
            var acc = ThisUser(map: map, password: oldPassword);
            //puts empty string if null
            acc.firstName = info.FirstName ?? "";
            acc.lastname = info.LastName ?? "";
            acc.address = info.Street ?? "";
            acc.zipCode = info.ZipCode ?? "";
            acc.city = info.City ?? "";
            acc.country = info.Country ?? "";
            acc.phoneNumber = info.PhoneNumber ?? "";
            if(! acc.email.Equals(info.Email) && info.Email != null) {
                acc.email = info.Email;
                acc.password = Account.hash(oldPassword, info.Email);
            }
            try {
                map.SubmitChanges();
            } catch (Exception) {  
                throw new FaultException("That email is not avaiable");
            }
            
        }

        public void ChangePassword(string oldPassword, string newPassword) {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var acc = ThisUser(oldPassword, map);
            if (string.IsNullOrEmpty(newPassword)) throw new FaultException(new FaultReason("Password cannot be null or empty"));
            acc.password = Account.hash(newPassword, account.Email);
            map.SubmitChanges();
        }

        public void DeleteMyAccount(string password) {
            checkIfLoggedIn();
            var dbSession = new db.DataMapDataContext();
            var acc = (from u in dbSession.Users
                       where u.id == account.Id
                          && u.password == Account.hash(password, account.Email)
                       select u).First();
            dbSession.Users.DeleteOnSubmit(acc);
            dbSession.SubmitChanges();
        }

        public Media ReadMedia(int id) {
            checkIfLoggedIn();
            var dbResult = from m in new db.DataMapDataContext().Medias
                           where m.id == id
                              && (m.published || m.owner == account.Id || account.Permission == PermissionGroup.Admin) //you need to be either owner or admin, or the media needs to be public.
                           select m;
            if (dbResult.Any()) return new Media(dbResult.First());
            throw new FaultException(new FaultReason("Media with id " + id + " not found."), new FaultCode("404"));
        }

        public List<Media> SearchProduct(SearchParameters parameters) {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var searchstring = parameters.SearchString ?? "";
            var words = searchstring.ToLower().Split(new char[] { ' ', ',' }).Except(new string[] { "" });
            if (!words.Any()) words = new string[] { "" }; // Default to empty string.
            var filterby = parameters.FilterBy ?? new List<MediaType>(0);
            var filterlist = filterby.Select(t => t.ToString());
            var dbresult = from m in map.Medias
                           where m.published &&
                           // Filter by availability
                           (parameters.Availability == SearchAvailability.CanBeRented ?
                           m.canBeRented :
                           parameters.Availability == SearchAvailability.CanBeBought ?
                           m.canBeBought : m.canBeRented || m.canBeBought) &&
                           // Filter by media type
                           (parameters.InclusiveFiltering ?
                           filterlist.Contains(m.mediaType) : !filterlist.Contains(m.mediaType))
                           select m;
            // Filter by search string
            var result = dbresult.ToList().Where(m => words.Any(w => SearchMatch(m, w)));
            // Order first by search string, then search order.
            var ordered = parameters.Ascending ? result.OrderBy(m => words.Count(w => this.SearchMatch(m, w))) :
                result.OrderByDescending(m => words.Count(w => SearchMatch(m, w)));
            switch (parameters.OrderBy) {
                case SearchOrder.NumberOfPurchases:
                    ordered = parameters.Ascending ? ordered.ThenBy(m => NumberOfPurchases(m.id)) :
                        ordered.ThenByDescending(m => NumberOfPurchases(m.id));
                    break;
                case SearchOrder.ReleaseDate:
                    ordered = parameters.Ascending ? ordered.ThenBy(m => m.releaseDate) :
                        ordered.ThenByDescending(m => m.releaseDate);
                    break;
                default:
                    ordered = parameters.Ascending ? ordered.ThenBy(m => m.title) :
                        ordered.ThenByDescending(m => m.title);
                    break;
            }
            return ordered.Take(parameters.ResultLimit == default(int) ? int.MaxValue : parameters.ResultLimit)
                .Select(m => new Media(m)).ToList();
        }

        private bool SearchMatch(db.Media m, string word) {
            return m.title.ToLower().Contains(word)
                || m.author.ToLower().Contains(word)
                || m.category.ToLower().Contains(word);
        }

        private int NumberOfPurchases(int id) {
            return (from p in new db.DataMapDataContext().Purchases
                    where p.media == id
                    select p).Count();
        }

        public void RentProduct(int id) {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var result = from dbMedia in map.Medias
                         where dbMedia.id == id
                            && dbMedia.published
                         select dbMedia;
            if (!result.Any() || result.First().rentingPrice < 0)
            {
                throw new FaultException(new FaultReason("Media with id " + id + " not found."), new FaultCode("404"));
            }
            var media = result.First();
            this.InsertPurchase(map, media, media.rentingPrice, DateTime.Now.AddHours(48));
        }

        public void BuyProduct(int id) {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var result = (from dbMedia in map.Medias
                          where dbMedia.id == id
                             && dbMedia.published
                          select dbMedia);
            if (!result.Any() || result.First().purchasePrice < 0)
            {
                throw new FaultException(new FaultReason("Media with id " + id + " not found."), new FaultCode("404"));
            }
            var media = result.First();
            this.InsertPurchase(map, media, media.purchasePrice, DateTime.MaxValue);
        }

        // Helper method for buy and rent product.
        private void InsertPurchase(db.DataMapDataContext map, db.Media media, decimal price, DateTime expires)
        {
            var user = (from u in map.Users
                        where u.id == account.Id
                        select u).First();
            if (user.credit < price)
            {
                throw new FaultException(new FaultReason("Insufficient credits for this purchase."));
            }
            user.credit -= price;
            map.Purchases.InsertOnSubmit(
                new db.Purchase
                    { date = DateTime.Now, expires = expires, price = price, media = media.id, user = user.id });
            map.SubmitChanges();
        }

        public List<Media> ReadDownloadableProducts() {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var result = from p in map.Purchases
                         where p.user == account.Id
                            && p.expires > DateTime.Now
                         join m in map.Medias on p.media equals m.id
                         select m;
            return result.Select(m => new Media(m)).ToList();
        }

        public List<Purchase> ReadPreviousPurchases() {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var purchases = from p in map.Purchases
                            where p.user == account.Id
                            join m in map.Medias on p.media equals m.id
                            select new { p, m };
            return purchases.Select(purc =>
                new Purchase {
                    Date = purc.p.date,
                    Expires = purc.p.expires,
                    Price = purc.p.price,
                    Product = new Media(purc.m)
                }).ToList();
        }

        public void AddCredit(decimal amount, CreditCardInformations creditCard, bool saveCreditCard) {
            checkIfLoggedIn();
            if (amount < 0) throw new FaultException(new FaultReason("Negative currency not gona happen."));
            var map = new db.DataMapDataContext();
            var user = (from u in map.Users
                        where u.id == account.Id
                        select u).First();
            user.credit += amount;
            if (saveCreditCard)
                map.CreditCards.InsertOnSubmit(new db.CreditCard
                {
                    cardType = creditCard.CardType,
                    expirationMonth = creditCard.ExpirationMonth,
                    expirationYear = creditCard.ExpirationYear,
                    number = creditCard.CardNumber,
                    verificationCode = creditCard.VerificationCode,
                    owner = user.id
                });
            map.SubmitChanges();
        }

        public void DeleteCreditCardInformation() {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var result = (from user in map.Users
                          where user.id == account.Id
                          select user).First().CreditCards;
            if (!result.Any())
            {
                return;
            }
            map.CreditCards.DeleteOnSubmit(result.First());
            map.SubmitChanges();
        }

        public int GetDownloadToken(int id) {
            checkIfLoggedIn();
            var map = new db.DataMapDataContext();
            var mediaRes = from p in map.Medias
                           where p.id == id
                           select p;
            if (!mediaRes.Any()) throw new FaultException(new FaultReason("Media does not exist"), new FaultCode("404"));
            if (mediaRes.First().owner != account.Id && account.Permission != PermissionGroup.Admin) {
                var result = from p in map.Purchases
                              where p.media == id
                                 && p.user == account.Id
                              select p;
                if (!result.Any())
                    throw new FaultException(new FaultReason("Media with id " + id + " not found, " +
                        "or the current user does not have permission to download it."),
                        new FaultCode("404"));
            }
            var download = new db.Download {
                media = id,
                expires = DateTime.Now.AddMinutes(30)
            };
            map.Downloads.InsertOnSubmit(download);
            map.SubmitChanges();
            return download.id;
        }
    }
}
